from __future__ import print_function

import stackless
import logging
import pickle
import traceback
import sys
import subprocess
import time
import cPickle as cpickle
import io
import argparse

from kharlia import core, engine, net, wxinter, gamewindow

from kharlia.engine import (
    CLevel,
    FSizeI, FVector3F, FVector2I, FTileData, FRectI, FRectF
)

GLogger = None
GCompleted = 0
GTicks = 0

def ReportMRO(cls, out=print):
    if not isinstance(cls, type):
        raise TypeError('not a class')
    out('MRO for: {0}.{1}'.format(cls.__module__, cls.__name__))
    for i, e in enumerate(cls.__mro__):
        out('{0}: {1}'.format(i, str(e)))

def ReportNativeObjects(out=print):
    out('== BEGIN OBJECT LIST ==')
    for i in core.CObject.IterObjects():
        out(str(i))
    out('== END OBJECT LIST ==')

def PrintTileData(td, out=print):
    tdl = td.ToList()
    i = 0
    out('== FTileData ==')
    for y in xrange(td.Size.Height):
        line = ''
        for x in xrange(td.Size.Width):
            line += str(tdl[i]) + ', '
            i += 1
        out(line)
    out('== End FTileData ==')

def MessageReader(conn):
    GLogger.debug('READING MESSAGE...')
    msg = conn.ReadMessage()
    GLogger.debug('READ MESSAGE: %s', msg)

class CNetTestListener(net.CNetListener):
    def OnConnectionAccepted(self, conn):
        core.CTasker.GetDefault().CreateTasklet(MessageReader)(conn)

class CTestGame(engine.CGame):
    def OnStartGame(self, world):
        GLogger.info('starting test game')
        super(CTestGame, self).OnStartGame(world)
        
    def OnStopGame(self):
        GLogger.info('stopping test game')
        super(CTestGame, self).OnStopGame()    

def Init():
    app = core.CApp.GetInstance()
    svcmgr = app.ServiceManager
    
    # Parse arguments
    parser = argparse.ArgumentParser(prefix_chars='-+', add_help=False)
    parser.add_argument('+Level', nargs=1, type=unicode,
                        default=[u'autoload.level'])
    parser.add_argument('+ClientTickRate', nargs=1, type=int,
                        default=[30])
            
    args, remaining = parser.parse_known_args(app.Args)
                
    # Get the services we need
    ensvc = svcmgr.GetServiceNoFail('kharlia.engine.CEngineService')
    wxsvc = svcmgr.GetServiceNoFail('kharlia.wxinter.CWXService')
        
    assert ensvc.IsStarted
    assert wxsvc.IsStarted
        
    def FrameCloseCallback():
        app.RequestExit()
        return True
        
    eng = ensvc.Engine
    # Create and set the top wxPython window with the basic frame class.
    frame = wxsvc.CreateTopWindow(
        gamewindow.CViewportFrame,
        app.Name,
        (eng.Config['Width'], eng.Config['Height']),
        FrameCloseCallback
    )
        
    # Initialize the client now that we have a viewport.
    # This is necessary because an OpenGL context must be created
    # for CEGUI to be initialized in the client. The client 
    # requires a valid target window to initialize the OpenGL
    # context, which the viewport provides.
    eng.InitClient(frame.Viewport)

    #yield_()
    
    # Switch for default test world setup        
    if 1:
        #print('ARGS:', args)
        # Create a default world, rather than loading one
        # from disk (not yet implemented).
        world = engine.CWorld()
        world.Init(eng)
        eng.World = world
    
        # App will sleep for one milisecond in each tick
        app.ThreadSleepTime = 1
        # Disable tick throttling for FPS, this is handled in
        # the engine for rendering.
        app.TickRate = 0
        # Time to wait for tasklets to run before exiting
        app.ExitWaitTime = 1.0
        # Desired client tick rate, which is effectively the
        # FPS for rendering
        eng.ClientTickRate = args.ClientTickRate[0]
        # Render bounding boxes for actors
        eng.Client.RnShowActorBounds = True
        # Configure the default tasklet group
        ops = app.Tasker.GetGroupOptions(0)
        ops.Timeout = 5000
        ops.SoftSwitch = True
        ops.TotalTimeout = True
            
        if 1:
            # Load autoload.level
            levfile = io.open(args.Level[0], 'rb')
            lev = engine.CLevel.LoadPackage(world, levfile)
            #yield_()    
            
        # Load a level from a TMX source file
        if 0:
            #file = open(r'data\levels\TestLevel.tmx')
            #lev = engine.LoadTMX(file, world)
            data = core.CIOManager.GetInstance().LoadFile(r'data\levels\TestLevel.tmx')
            lev = engine.LoadTMX(data, world)
            #yield_()
                    
        # Create a level manually
        if 0:
            lev = world.CreateLevel(None, u'TestLevel', FSizeI(2, 2))
            lev.CreateLayer(0)
            # Default to grass
            lev.SetTiles(0, 16)
            # Place a bush at 2x2
            bushtiles = FTileData((2, 2), (74, 75, 90, 91))
            lev.SetTiles(0, (3, 3), bushtiles)
            #engine._engine.ComponentTest(eng, world, lev)
            #act = lev.GetActor(u'TestCamera')
            
        # Specify game type and start it
        world.GameClass = CTestGame
        world.StartGame()
                
        pc = world.CreatePlayerActor(eng.LocalPlayer)
            
        # Populate the level with our test actors
        comps = (
            engine.ATransform,
            engine.AMovement,
            engine.APhysics,
            engine.ACamera,
            engine.ARender,
            engine.APawn
            )
                    
        act = lev.CreateActor(comps)
        act.Tag = u'TestActor'
        #apctype = engine.APlayerController
        #pcact = lev.CreateActor(apctype, name=u'TestPlayerController')
                
        trans = act.Transform
        trans.Position = engine.FVector3F(6.0, 6.0, 0.0)
                
        cam = act.Camera
        # Set the camera the client will use to render the scene
        eng.Client.Camera = cam
        # Set the current test actor, which is currently used for
        # movement
        eng.TestActor = act
        eng.TestMovement = act.Movement
                
        # Test 0 is tileset test, need it for collision
        lev.Test(0)
        phys = act.Physics
        phys.Bounds = FRectF(0, 0, 2, 2)
        phys.CollideTiles = True
        phys.CollideActors = True
        phys.CollisionLayer = 0
                
        #pc = pcact.GetComponent(apctype)
        pawn = act.GetComponent(engine.APawn)
        assert pc is not None
        assert pawn is not None
        pc.Possess(pawn)
        assert pawn.Controller == pc
        assert pc.Pawn == pawn
            
        # New pickle testing using components
        if 0:
            s = engine.CActor.Pickle(act, cpickle.dumps, cpickle.HIGHEST_PROTOCOL)
            act2 = engine.CActor.Unpickle(s, lev, cpickle.loads)
            act2.Tag = u'TestCamera2'
            trans2 = act2.GetTransform()
            assert trans2
            print(trans2.Position)
            print(trans is trans2)
            phys2 = act2.GetPhysics()
            print(phys2.Bounds)
            print(phys2.CollideTiles)
            print(phys2.CollideActors)
            assert phys2.Transform is trans2

def PerformTests():
    # Note: Tests may be outdated and left in for reference

    # New network component testing
    if 0:
        net = engine.ANetwork()
        act.AddComponent(net)
        assert net
        print('ACTOR LEN:', len(act))
        net.InitPythonRep()
        for i in xrange(3):
            start = time.clock()
            rn = net.ReplicatePython()
            elapsed = (time.clock() - start) * 1000
            print('RESULTTIME (MS):', elapsed)
            if rn is not None:
                print('RESULTS:')
                for rcls in rn:
                    print(rcls)
            else:
                print('RESULTS: None')

    # CTasker improvements test
    if 0:
        tasker = app.Tasker
        tlist = tasker.GetTaskletList(0)
        print('tasklet list:')
        for i in tlist:
            print(i)
        print('end list')
            
    # Boost.Python unicode type wrapper test
    if 0:
        from kharlia._core import TestUnicode
        TestUnicode(u'lol ninja pancakes')
                
    # Special Extract test
    if 0:
        from kharlia._core import TestExtract
        TestExtract(app)

    # TMX Load testing
    if 0:
        sleep(1.0)
        file = open(r'data\levels\TestLevel.tmx')
        engine.LoadTMX(file, world)
                
    # Actor spawn testing
    if 0:
        sleep(2.0)
        comps = (
            engine.ATransform,
            engine.AMovement,
            engine.APhysics,
            engine.ACamera,
            #engine.ARender,
            engine.APawn
            )
        GLogger.info('BEGIN ACTOR SPAWNING')
        #engine.GLogger.setLevel(logging.INFO)
        spawns = 3000
        perloop = 10
        start = time.clock()
        c = 0
        for i in xrange(spawns):
            lev.CreateActor(comps)
            if c > perloop:
                yield_()
                c = 0
            else:
                c += 1
        elapsed = time.clock() - start
        elapsedper = elapsed / spawns
        GLogger.info('END ACTOR SPAWNING: %s (%s per actor)', elapsed, elapsedper)

    # Actor variable data
    if 0:
        print('ACTOR VARS:')
        dvs = []
        for k, v in cam.__dict__.iteritems():
            dvs.append(k)
            
        for c in type(cam).mro():
            for k, v in c.__dict__.iteritems():
                if (hasattr(v, '__get__') and
                    (hasattr(v, '__set__') or hasattr(v, '__delete__')) and
                    not (k.startswith('__') and k.endswith('__'))):
                    dvs.append(k)

        for i in dvs:
            print(i)

    # Actor reference testing
    #   Involves finding references to other actors in and outside
    #   of the level
    if 0:
        import gc
        sleep(1.0)
        print('Beginning actor reference testing')

        target = lev

        actors = []
        for i in target.IterActors():
            refs = []
            for e in gc.get_referents(i):
                if isinstance(e, engine.AActor):
                    refs.append(e)
            actors.append((i, refs))

        print('actor refs found:')
        for actor, refs in actors:
            print(actor, '=', refs)


    # GC Reference testing
    if 0:
        import gc
        from kharlia._core import TestFileIODump
        print('GC enabled:', gc.isenabled())
        sleep(4.0)
        print('DUMPING GC INFO')

        dumptarget = lev

        outlines = []
        outlines.append('=== referrers ===')
        for i in gc.get_referrers(dumptarget):
            outlines.append(''.join((str(type(i)), ' = ', str(i))))
        outlines.append('=== referrents ===')
        for i in gc.get_referents(dumptarget):
            outlines.append(''.join((str(type(i)), ' = ', str(i))))
            #print('referents:', i)
        outlines = '\n'.join(outlines)
        TestFileIODump('gcinfo.txt', outlines)
        print('FINISHED DUMP')

    # Native GC macro testing
    if 0:
        sleep(2.0)
        import gc
        gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_COLLECTABLE | \
                        gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_INSTANCES | \
                        gc.DEBUG_OBJECTS)
        act10 = lev.CreateActor(ACamera, name='GCTest1')
        act11 = lev.CreateActor(ACamera, name='GCTest2')
        act10.Target = act11
        act11.Target = act10
        lev.DestroyActor(act10)
        lev.DestroyActor(act11)
        del act10
        del act11
        sleep(2.0)
        while gc.collect():
            pass

    # Layer testing
    if 0:
        #info = lev.GetLayerInfo(0)
        #info.Visible = False
        #sleep(2.0)
        #info.Visible = True
        info1 = lev.CreateLayer(1)
        sleep(2.0)
        info1.Alpha = 128

    # Actor creation Testing
    if 0:
        sleep(3.0)
        start = time.clock()
        act3 = lev.CreateActor(AActor, owner=act,
                                pos=FVector3F(2.0, 2.0, 2.0))
        elapsed = time.clock() - start
        GLogger.debug('created actor in %s miliseconds', elapsed * 1000)

    # Stackless IO Testing
    if 0:
        testnum = 50
        sleep(2.0)

        def iotest():
            global GCompleted
            start = time.clock()
            filestr = core.native.TestFileIO('jsontest.json')
            elapsed = time.clock() - start
            print('FILE IO ELAPSED TIME:', elapsed)
            print('FILE IO RESULT LEN:', len(filestr))
            GCompleted += 1

        starto = time.clock()
        for i in xrange(testnum):
            app.CreateTasklet(iotest)()
        while GCompleted < testnum:
            GTicks += 1
            print('TICK:', GTicks)
            yield_()

        elapsedo = time.clock() - starto
        print('TOTAL COMPLETION TIME:', elapsedo)
        print('TICKS NEEDED TO COMPLETE:', GTicks)
        #print('FILE IO RESULT:', filestr)
        rpt = core.native.StatReportDispatch()
        print('DISPATCH REPORT: count=%s, totaltime=%s, avgtime=%s' % rpt)
        dumpstr = 'lol testing dump something'
        core.native.TestFileIODump('iotest_dump.txt', dumpstr)

    # Async IO Testing
    if 0:
        def CBTest():
            print('CALLED THE CALLBACK!')

        dumpstr = 'lol testing dump something'
        core.native.TestFileIODumpCB('test/iotest_dumpcb.txt', dumpstr, CBTest)
        #core.native.TestFileIODumpCBC('test/iotest_dumpcb.txt', dumpstr)

    # Tiledata copy testing
    if 0: 
        td = FTileData(FSizeI(4, 4), (1, 2, 3, 4,
                                        5, 6, 7, 8,
                                        9, 10, 11, 12,
                                        13, 14, 15, 16))
        tdl = td.ToList()
        #print(tdl)
        PrintTileData(td)
        td2 = FTileData(FSizeI(4, 4), (20, 21, 22, 23,
                                        24, 25, 26, 27,
                                        28, 29, 30, 31,
                                        32, 33, 34, 35))
        PrintTileData(td2)
        td2.CopyTiles(FVector2I(0, 0), FRectI(0, 0, 4, 4), td)
        PrintTileData(td2)

    # Replication testing
    if 0:
        #act.NetDirty = True
        #svcmgr['replication'].RegisterActor(act)
        rep = engine.CActorReplicator()
        rep.BindActor(cam)

        start = time.clock()
        rep.ReplicatePython()
        elapsed = time.clock() - start
        print('elapsed:', elapsed)

        sleep(1.0)

        start = time.clock()
        rep.ReplicatePython()
        elapsed = time.clock() - start
        print('elapsed:', elapsed)

        sleep(1.0)

        cam.Dir = engine.DIR_WEST
        cam.TestVar = 'superpancake'

        start = time.clock()
        rep.ReplicatePython()
        elapsed = time.clock() - start
        print('elapsed:', elapsed)

    # Native replication testing
    if 0:
        start = time.clock()
        rstr = rep.ReplicateNative()
        elapsed = time.clock() - start
        print('rtime:', elapsed * 1000)
        if rstr is not None:
            print('rstrlen:', len(rstr))
        print('rstr:', rstr)

        cam.Position = (3.0, 4.0, 0.0)
        cam.TargetOffset = (1.0, 1.0, 0.0)

        start = time.clock()
        rstr = rep.ReplicateNative()
        elapsed = time.clock() - start
        print('rtime:', elapsed * 1000)
        if rstr is not None:
            print('rstrlen:', len(rstr))
        print('rstr:', rstr)

        start = time.clock()
        rstr = rep.ReplicateNative()
        elapsed = time.clock() - start
        print('rtime:', elapsed * 1000)
        if rstr is not None:
            print('rstrlen:', len(rstr))
        print('rstr:', rstr)

        start = time.clock()
        rstr = rep.ReplicateNative()
        elapsed = time.clock() - start
        print('rtime:', elapsed * 1000)
        if rstr is not None:
            print('rstrlen:', len(rstr))
        print('rstr:', rstr)

    # Basic pickle testing
    if 0:
        td = FTileData(FSizeI(4, 4), (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16))
        print(td)
        ptd = pickle.dumps(td, pickle.HIGHEST_PROTOCOL)
        print('DUMP SIZE:', len(ptd))
        td2 = pickle.loads(ptd)
        print(td2)

    # Level pickle testing
    if 0:
        import zlib

        # Simpler tilegroup pickle testing
        if 0:
            tg = lev.GetTileGroup(0)
            print(tg)
            ptg = pickle.dumps(tg, pickle.HIGHEST_PROTOCOL)
            print('DUMP SIZE:', len(ptg))
            tg2 = pickle.loads(ptg)
            print(tg2)
            print('LC:', tg2.LayerCount)
            print('SZ:', tg2.Size)
            print('L0 Index:', tg2.GetLayer(0).Index)
            print('L0 TileData:', tg2.GetLayer(0).TileData)

        print(lev)
        plev = engine.CLevel.Pickle(lev)
        print('LEVEL DUMP SIZE:', len(plev))
        zplev = zlib.compress(plev)
        print('COMPRESSED LEVEL DUMP SIZE:', len(zplev))
        dzplev = zlib.decompress(zplev)
        print('DECOMPRESSED LEVEL DUMP SIZE:', len(dzplev))
        world.DestroyLevel(lev)
        print('DESTROYED LEVEL')

        sleep(2.0)

        print('LOADING LEVEL...')
        lev2 = engine.CLevel.Unpickle(world, plev)
        print('LOADED LEVEL')
        print(lev2)
        if 1:
            # Transient camera
            cam2 = lev2.CreateActor(ACamera, name='MainCameraX', pos=(5, 5, 5))
            print('CAM2:', cam2)
            eng.ClientCamera = cam2
            eng.TestActor = lev2.GetActor('AActor_1')
            cam2.Target = eng.TestActor

        else:
            # Non-Transient camera
            cam2 = world.GetActor('MainCamera')
            print('CAM2:', cam2)
            eng.ClientCamera = cam2
            eng.TestActor = lev2.GetActor('AActor_1')


        #print(cam)
        #pcam = pickle.dumps(cam, pickle.HIGHEST_PROTOCOL)
        #print('DUMP SIZE:', len(pcam))
        #cam2 = pickle.loads(pcam)
        #print(cam2)

    # Level package
    if 0:
        engine.CLevel.SavePackage(lev, 'test/levelpackage.khl')

    # RakNet testing (start client)
    if 0:
        world.NetRepRate = 5
        if '-raktest' in sys.argv:
            frame.Title = frame.Title + u' (NETWORK CLIENT)'
            GLogger.info('NETWORK CLIENT STARTED')
            GLogger.info('attempting connnection to server')
            world.NetConnect('localhost', 60500)
                    
        else:
            frame.Title = frame.Title + u' (NETWORK SERVER)'
            GLogger.info('NETWORK SERVER STARTED')
            world.NetListen()
            GLogger.info('server now listening')

            obj = subprocess.Popen('cmd /C start /WAIT bin\\Kharlia_d.exe -raktest')

            sleep(5.0)
            world.NetSendDebugMessage('HELLO, HOW ARE YOU?!')
            nact = lev.CreateActor(AActor, name=u'TestNetActor',
                                    pos=FVector3F(3.0, 3.0, 0.0))
            nact.RemoteRole = engine.ROLE_SIM_PROXY
            nact.Bounds = FRectF(0, 0, 2, 2)
            nact.CollideTiles = True
            nact.CollideActors = True
            eng.TestActor = nact
                    
            sleep(3.0)
            #GLogger.info('beginning RPC send')
            #world.NetSendActorRPC(True, nact, 'DisplayRPCMessage', 'hello 123')
            #nact.ClientDisplayRPCMessage('lol pancakes')
            #nact.ServerDisplayRPCMessage('server to server')
            #GLogger.info('finished RPC send')
                    
            #sleep(5.0)
            #nact.RemoteRole = engine.ROLE_NONE

    # RakNet testing (start server)
    if 0:
        world.NetRepRate = 5
        if '-raktest' in sys.argv:
            frame.Title = frame.Title + u' (NETWORK SERVER)'
            GLogger.info('NETWORK SERVER STARTED')
            world.NetListen()
            GLogger.info('server now listening')
                    
            sleep(5.0)
            nact = lev.CreateActor(AActor, name=u'TestNetActor',
                                    pos=FVector3F(3.0, 3.0, 0.0))
            nact.RemoteRole = engine.ROLE_SIM_PROXY
            nact.Bounds = FRectF(0, 0, 2, 2)
            nact.CollideTiles = True
            nact.CollideActors = True
            eng.TestActor = nact
                    
            sleep(3.0)
            if 0:
                nact.ClientDisplayRPCMessage('server to client')
                nact.ServerDisplayRPCMessage('server to server')
                GLogger.info('beginning RPC send')
                world.NetSendActorRPC(True, nact, 'DisplayRPCMessage', 'hello 123')
                GLogger.info('finished RPC send')
                    
        else:
            frame.Title = frame.Title + u' (NETWORK CLIENT)'
            GLogger.info('NETWORK CLIENT STARTED')
            obj = subprocess.Popen('cmd /C start /WAIT bin\\Kharlia_d.exe -raktest')
                    
            sleep(5.0)
            GLogger.info('attempting connnection to server')
            world.NetConnect('localhost', 60500)
                    
            sleep(5.0)
            if 0:
                nact = lev.GetActor(u'TestNetActor')
                if nact is not None:
                    nact.ServerDisplayRPCMessage('client to server')
                    nact.ClientDisplayRPCMessage('client to client')
                else:
                    print('TestNetActor not found')
            #world.NetSendDebugMessage('HELLO, HOW ARE YOU?!')

    # Simulated function
    if 0:
        act.SimTest()
        act.SimTestN()
        act.Role = engine.ROLE_SIM_PROXY
        act.SimTest()
        act.SimTestN()
        # TODO: Not setting the Role back causes slight delay
        #       on exit. Wtf?
        act.Role = engine.ROLE_AUTHORITY

    ## END TESTING
        
def TestGameMain():
    global GLogger

    # Mark the current tasklet critical so an uncaught exception will
    # cause the program to exit.
    core.SetCurrentTaskletCritical(True)

    GLogger = logging.getLogger('testgame')
    GLogger.info('Initializing test game')
    try:
        app = core.CApp.GetInstance()
        sleep = core.Sleep
                
        Init()
        
        sleep(1.0)
        
        PerformTests()

        while not app.ExitRequested:
            ## Nothing to do here yet
            sleep(0.2)
    finally:
        GLogger.info('Exiting test game')